using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace JTLeigh.Tools.Development.CopySourceAsHtml
{

    /// <summary>
    /// Base class for RTF readers.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This class parses RTF strings and calls its <see langword="abstract"/> methods to read 
    /// higher level structures like color table entries, grouping, foreground and background 
    /// coloring, bolding, characters, newlines, etc.
    /// </para>
    /// <para>
    /// This class handles character escaping and encoding, so inheriting classes deal only with
    /// Unicode characters.
    /// </para>
    /// </remarks>
    public abstract class RtfReader
    {
        #region Fields

        private static Regex _colorPattern = new Regex(@"\\red(?<r>\d+)\\green(?<g>\d+)\\blue(?<b>\d+)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);

        private Encoding _encoding;

        #endregion // Fields

        #region Methods

        /// <summary>
        /// Parses an RTF string and calls <see cref="RtfReader"/>'s <see langword="abstract"/> 
        /// methods to read higher level structures like color table entries, grouping, foreground 
        /// and background coloring, bolding, characters, newlines, etc.
        /// </summary>
        /// <param name="rtf">The RTF string.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="rtf"/> is 
        /// <see langword="null"/>.</exception>
        protected internal void Read(string rtf)
        {

            int lastCharacterIndex;
            int currentCharacterIndex;
            int bookmarkIndex;
            string controlWord;
            bool hasParameter;
            long parameterValue;

            Safety.AssertArgumentNotNull("rtf", rtf);

            Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, "Parsing RTF: {0}", rtf));
            Debug.Indent();

            // NOTE: We want to reset the state of the reader between each read.

            _encoding = Encoding.Default;

            // NOTE: RTF has the syntax '{' + header + body + '}', and we only want to parse the
            // header and body, so we skip the leading and trailing braces by starting at index 1
            // and ending before rtf.Length - 1.

            lastCharacterIndex = rtf.Length - 1;
            for (currentCharacterIndex = 1; currentCharacterIndex < lastCharacterIndex; currentCharacterIndex++)
            {

                switch (rtf[currentCharacterIndex])
                {
                    case '{':
                        ReadBeginGroup();
                        break;
                    case '}':
                        ReadEndGroup();
                        break;
                    case '\\':
                        currentCharacterIndex++;
                        if (Char.IsLetter(rtf[currentCharacterIndex]))
                        {
                            bookmarkIndex = currentCharacterIndex;
                            while ((currentCharacterIndex < lastCharacterIndex) && Char.IsLetter(rtf[currentCharacterIndex]))
                            {
                                currentCharacterIndex++;
                            }
                            controlWord = rtf.Substring(bookmarkIndex, currentCharacterIndex - bookmarkIndex);
                            if ((currentCharacterIndex < lastCharacterIndex) && (Char.IsDigit(rtf[currentCharacterIndex]) || (rtf[currentCharacterIndex] == '-')))
                            {
                                bookmarkIndex = currentCharacterIndex;
                                while ((currentCharacterIndex < lastCharacterIndex) && (Char.IsDigit(rtf[currentCharacterIndex]) || (rtf[currentCharacterIndex] == '-')))
                                {
                                    currentCharacterIndex++;
                                }
                                hasParameter = true;
                                parameterValue = Int64.Parse(rtf.Substring(bookmarkIndex, currentCharacterIndex - bookmarkIndex), CultureInfo.InvariantCulture);
                            }
                            else
                            {
                                hasParameter = false;
                                parameterValue = 0;
                            }
                            if (rtf[currentCharacterIndex] != ' ')
                            {
                                currentCharacterIndex--;
                            }
                            currentCharacterIndex += ParseControlWord(rtf, currentCharacterIndex + 1, lastCharacterIndex, controlWord, hasParameter, parameterValue);
                        }
                        else
                        {
                            currentCharacterIndex += ParseControlSymbol(rtf, currentCharacterIndex + 1, lastCharacterIndex, rtf[currentCharacterIndex]);
                        }
                        break;
                    case '\r':
                    case '\n':
                        break;
                    default:
                        ReadCharacter(rtf[currentCharacterIndex]);
                        break;
                }
            }

            Debug.Unindent();
            Debug.WriteLine("Parsing complete.");

        }

        /// <summary>
        /// Parses an RTF control word.
        /// </summary>
        /// <param name="rtf">The RTF string.</param>
        /// <param name="nextCharacterIndex">The next character in the RTF string to parse.</param>
        /// <param name="lastCharacterIndex">The last character in the RTF string.</param>
        /// <param name="controlWord">The control word.</param>
        /// <param name="hasParameter"><see langword="true"/> if the control word has a parameter,
        /// <see langword="false"/> otherwise.</param>
        /// <param name="parameterValue">The value of the parameter, if the control word has one.</param>
        /// <returns>The number of characters to advance in the RTF string.</returns>
        protected virtual int ParseControlWord(string rtf, int nextCharacterIndex, int lastCharacterIndex, string controlWord, bool hasParameter, long parameterValue)
        {
            Safety.AssertArgumentNotNull("rtf", rtf);
            Safety.AssertArgumentNotNull("controlWord", controlWord);

            switch (controlWord)
            {
                case "fonttbl":
                    return ParseFontTable(rtf, nextCharacterIndex, lastCharacterIndex);
                case "colortbl":
                    return ParseColorTable(rtf, nextCharacterIndex, lastCharacterIndex);
                case "ansicpg":
                    // NOTE: The RTF standard says that \ansicpgN should have a parameter, but the
                    // RTF generated by Visual Studio sometimes doesn't have one.  If we have a 
                    // parameter, we'll use it to set the encoding.  Otherwise, we'll use the 
                    // default encoding, set elsewhere.
                    if (hasParameter)
                    {
                        _encoding = Encoding.GetEncoding((int)parameterValue);
                    }
                    return 0;
                case "par":
                    ReadNewLine();
                    return 0;
                case "tab":
                    ReadCharacter('\t');
                    return 0;
                case "cf":
                    Safety.AssertIsTrue(hasParameter, String.Format(CultureInfo.InvariantCulture, @"The control word \cf at position {0} does not have a parameter.", nextCharacterIndex));
                    ReadForegroundColor((int)parameterValue);
                    return 0;
                case "cb":
                    Safety.AssertIsTrue(hasParameter, String.Format(CultureInfo.InvariantCulture, @"The control word \cb at position {0} does not have a parameter.", nextCharacterIndex));
                    ReadBackgroundColor((int)parameterValue);
                    return 0;
                case "b":
                    // NOTE: \b turns bold on, \b0 turns bold off.
                    if (hasParameter && ((int)parameterValue == 0))
                    {
                        ReadBold(false);
                    }
                    else
                    {
                        ReadBold(true);
                    }
                    return 0;
                case "u":
                    ReadCharacter((char)parameterValue);
                    // NOTE: \uN is immediately followed by the ANSI representation of the Unicode 
                    // character.  We support Unicode characters, so we want to skip the ANSI
                    // representation, and so we return 1 to advance past it in the RTF string.
                    return 1;
                    // NOTE: We *should* be returning the parameter value of the last \ucN control 
                    // word that was parsed in the current group, but that would require keeping 
                    // track of groups and their \ucN values, which we currently don't do.
                case "rtf":
                case "lang":
                case "noproof":
                case "uc":
                    // We can safely ignore all of these.
                    return 0;
                default:
                    Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, @"The control word \{0} at position {1} was skipped.", controlWord, nextCharacterIndex));
                    return 0;
            }
        }

        /// <summary>
        /// Parses an RTF control symbol.
        /// </summary>
        /// <param name="rtf">The RTF string.</param>
        /// <param name="nextCharacterIndex">The next character in the RTF string to parse.</param>
        /// <param name="lastCharacterIndex">The last character in the RTF string.</param>
        /// <param name="controlSymbol">The control symbol to parse.</param>
        /// <returns>The number of characters to advance in the RTF string.</returns>
        /// <remarks>
        /// <para>
        /// RTF escapes printable characters that have special meaning by prefixing them with a
        /// backslash (e.g. \\, \{, \}).  This method calls <see cref="ReadCharacter"/> to read 
        /// these characters
        /// </para>
        /// <para>
        /// RTF also encodes 8-bit ANSI characters as hexadecimal in the form \'xx.  This method 
        /// calls <see cref="Parse8BitCharacter"/> to parse these characters.
        /// </para>
        /// </remarks>
        protected virtual int ParseControlSymbol(string rtf, int nextCharacterIndex, int lastCharacterIndex, char controlSymbol)
        {
            Safety.AssertArgumentNotNull("rtf", rtf);

            switch (controlSymbol)
            {
                case '\\':
                case '{':
                case '}':
                    ReadCharacter(controlSymbol);
                    return 0;
                case '\'':
                    return Parse8BitCharacter(rtf, nextCharacterIndex, lastCharacterIndex);
                default:
                    Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, @"The control symbol \{0} at position {1} was skipped.", controlSymbol, nextCharacterIndex));
                    return 0;
            }
        }

        /// <summary>
        /// Parses the RTF font table.
        /// </summary>
        /// <param name="rtf">The RTF string.</param>
        /// <param name="nextCharacterIndex">The next character in the RTF string to parse.</param>
        /// <param name="lastCharacterIndex">The last character in the RTF string.</param>
        /// <returns>The number of characters to advance in the RTF string.</returns>
        /// <remarks>
        /// <para>
        /// DELETEME doesn't use the font table, but it needs to parse it so that it 
        /// can advance past it in the RTF string.
        /// </para>
        /// <para>
        /// This method simply advances past the font table in the RTF string.
        /// </para>
        /// </remarks>
        internal static int ParseFontTable(string rtf, int nextCharacterIndex, int lastCharacterIndex)
        {
            int currentCharacterIndex;
            int depth;

            Safety.AssertArgumentNotNull("rtf", rtf);

            currentCharacterIndex = nextCharacterIndex;
            depth = 1;
            while ((currentCharacterIndex < lastCharacterIndex) && (depth > 0))
            {
                switch (rtf[currentCharacterIndex])
                {
                    case '{':
                        depth++;
                        break;
                    case '}':
                        depth--;
                        break;
                }
                currentCharacterIndex++;
            }
            return (currentCharacterIndex - nextCharacterIndex);
        }

        /// <summary>
        /// Parses the RTF color table.
        /// </summary>
        /// <param name="rtf">The RTF string.</param>
        /// <param name="nextCharacterIndex">The next character in the RTF string to parse.</param>
        /// <param name="lastCharacterIndex">The last character in the RTF string.</param>
        /// <returns>The number of characters to advance in the RTF string.</returns>
        /// <remarks>
        /// <para>
        /// This method calls <see cref="ReadColorTableEntry"/> for each color table entry.
        /// </para>
        /// </remarks>
        internal int ParseColorTable(string rtf, int nextCharacterIndex, int lastCharacterIndex)
        {
            Match colorMatch;
            int currentCharacterIndex;
            int colorId;
            int mark;
            int redLevel;
            int greenLevel;
            int blueLevel;

            Safety.AssertArgumentNotNull("rtf", rtf);

            currentCharacterIndex = nextCharacterIndex;
            colorId = 0;
            mark = currentCharacterIndex;
            while ((currentCharacterIndex < lastCharacterIndex) && (rtf[currentCharacterIndex] != '}'))
            {
                if (rtf[currentCharacterIndex] == ';')
                {
                    colorMatch = _colorPattern.Match(rtf.Substring(mark, currentCharacterIndex - mark));
                    if (colorMatch.Success)
                    {
                        redLevel = Convert.ToInt32(colorMatch.Groups["r"].Value, CultureInfo.InvariantCulture);
                        greenLevel = Convert.ToInt32(colorMatch.Groups["g"].Value, CultureInfo.InvariantCulture);
                        blueLevel = Convert.ToInt32(colorMatch.Groups["b"].Value, CultureInfo.InvariantCulture);
                        ReadColorTableEntry(colorId, Color.FromArgb(redLevel, greenLevel, blueLevel));
                    }
                    colorId++;
                    mark = currentCharacterIndex + 1;
                }
                currentCharacterIndex++;
            }
            return (currentCharacterIndex - nextCharacterIndex);
        }

        /// <summary>
        /// Parses 8-bit ANSI characters encoded as hexadecimal in the form \'x or \'xx.
        /// </summary>
        /// <param name="rtf">The RTF string.</param>
        /// <param name="nextCharacterIndex">The next character in the RTF string to parse.</param>
        /// <param name="lastCharacterIndex">The last character in the RTF string.</param>
        /// <returns>The number of characters to advance in the RTF string.</returns>
        /// <remarks>
        /// <para>
        /// RTF encodes 8-bit ANSI characters as hexadecimal in the form \'xx.  This method parses
        /// this form, but also parses the form \'x and treats \'0 as a space (' '), both of which
        /// aren't are seen in the RTF generated by Visual Studio.
        /// </para>
        /// </remarks>
        internal int Parse8BitCharacter(string rtf, int nextCharacterIndex, int lastCharacterIndex)
        {
            byte byteValue;
            int offset;
            char[] charArray;

            Safety.AssertArgumentNotNull("rtf", rtf);

            byteValue = 0;
            offset = 0;

            if (((nextCharacterIndex + 1) < lastCharacterIndex) && char.IsLetterOrDigit(rtf, nextCharacterIndex) && char.IsLetterOrDigit(rtf, (nextCharacterIndex + 1)))
            {
                byteValue = Byte.Parse(rtf.Substring(nextCharacterIndex, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                offset = 2;
            }
            else if (char.IsLetterOrDigit(rtf, nextCharacterIndex))
            {
                byteValue = Byte.Parse(rtf.Substring(nextCharacterIndex, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                if (byteValue == 0)
                {
                    // NOTE: We map \'0 to a space.
                    byteValue = 32;
                }
                offset = 1;
            }

            if (byteValue != 0)
            {
                charArray = _encoding.GetChars(new byte[] { byteValue });
                ReadCharacter(charArray[0]);
            }

            return offset;
        }

        /// <summary>
        /// Called when a color table entry is read.
        /// </summary>
        /// <param name="colorId">The color identifier.</param>
        /// <param name="color">The color.</param>
        protected internal abstract void ReadColorTableEntry(int colorId, Color color);

        /// <summary>
        /// Called when a group beginning is read.
        /// </summary>
        protected internal abstract void ReadBeginGroup();

        /// <summary>
        /// Called when a group ending is read.
        /// </summary>
        protected internal abstract void ReadEndGroup();

        /// <summary>
        /// Called when a foreground color change is read.
        /// </summary>
        /// <param name="colorId">The new foreground color identifier.</param>
        protected internal abstract void ReadForegroundColor(int colorId);

        /// <summary>
        /// Called when a background color change is read.
        /// </summary>
        /// <param name="colorId">The new background color identifier.</param>
        protected internal abstract void ReadBackgroundColor(int colorId);

        /// <summary>
        /// Called when a bold change is read.
        /// </summary>
        /// <param name="bold"><see langword="true"/> if bold is being turned on, 
        /// <see langword="false"/> if bold is being turned off.</param>
        protected internal abstract void ReadBold(bool bold);

        /// <summary>
        /// Called when a character is read.
        /// </summary>
        /// <param name="character">The character.</param>
        /// <remarks>
        /// <para>
        /// This method is called for every printable character, including spaces (' ') and tabs 
        /// ('\t'), but not for newlines.  The <see cref="ReadNewLine"/> method is called for every
        /// newline.
        /// </para>
        /// </remarks>
        protected internal abstract void ReadCharacter(char character);

        /// <summary>
        /// Called when a newline is read.
        /// </summary>
        protected internal abstract void ReadNewLine();

        #endregion // Methods
    }

}
